Encrypting Login Password without SSL in Ruby on Rails

For a personal project, I am building a Rails site that has an administration section. Of course, I don’t want any nefarious person who snoops my network traffic to be able to login. SSL isn’t an easy option because (1) my site is on a shared host, (2) I don’t want to pay for an SSL certificate, and (3) I would prefer that my users do not need to accept a self-signed certificate.

Given these conditions, I felt that a private/public key pair would successfully obfuscate login credentials without SSL. At a high level, my Rails application generates a 1024-bit RSA key on the fly and shares a public version with the client. The client utilizes an open source RSA library for JavaScript to encrypt the credentials on the client before sending them back to the server, which then uses the private key to decrypt them. I’m not an encryption expert, but I think the worst that could happen is that someone could decrypt the credentials for the one request they capture (feel free to correct me though).

Let’s get to the code. To set the situation, I am following REST conventions for authentication, so I have a SessionsController with a new action and a create action. The former is responsible for setting up the login and the latter for processing the user’s input.

First, the “new” action, which creates the RSA key, provides the public components to the view template, and stores the key (in PEM format) in session:

  def new
    key = OpenSSL::PKey::RSA.new(1024)
    @public_modulus  = key.public_key.n.to_s(16)
    @public_exponent = key.public_key.e.to_s(16)
    session[:key] = key.to_pem
  end

Then in the view template (“new.html.erb”), we provide the public modulus and exponent (the necessary component of the public key) as well as input forms for the username and password:

  <%= javascript_include_tag('rsa/jsbn', 'rsa/prng4', 'rsa/rng', 'rsa/rsa', 'rsa/base64', :cache => true) %>

  <% form_tag session_path, :id => 'login' do -%>
  <fieldset>
    <legend>Please Login</legend>
    <label for="login" class="required">Login</label>
    <%= text_field_tag :username, params[:username] %><br />
    <label for="password" class="required">Password</label>
    <%= password_field_tag :upassword, params[:upassword] %><br />
    <%= hidden_field_tag :password, '' %>
  </fieldset>
  <%= submit_tag 'Log in' %>
  <% end -%>

  <%= hidden_field_tag :public_modulus, @public_modulus %>
  <%= hidden_field_tag :public_exponent, @public_exponent %>

Two things to note here. First, we are including the four necessary JavaScript libraries on this page only. Second, we use a hidden field to store/commit the password – this field is populate via JavaScript.

My application utilizes jQuery, so attaching a function to encrypt the password before form submission is straightforward:

  $(document).ready(function() {
    $("form#login").submit(function() {
      var rsa = new RSAKey();
      rsa.setPublic($('#public_modulus').val(), $('#public_exponent').val());
      var res = rsa.encrypt($('#upassword').val());
      if (res) {
        $('#password').val(hex2b64(res));
        $('#upassword').val('');
        return true;
      }
      return false;
    })
  });

Before submission occurs, we encrypt the value of the “upassword” field, store an encrypted Base64 version in “password,” and clear “upassword.” If there is a problem, the form is not submitted.

On the server-side, this form is submitted to the SessionsController#create action:

  def create
    key = OpenSSL::PKey::RSA.new(session[:key])
    password = key.private_decrypt(Base64.decode64(params[:password]))
    user = User.authenticate(params[:username], password)
    if user
      reset_session  # reset session after login
      session[:user_id] = user.id
      flash[:notice] = "Welcome back, #{user.username}"
      redirect_to admin_url
    else
      flash[:error] = 'Invalid username/password entered'
      new and render :action => 'new'
    end
  end

Here, we pull the key out of session and use it to decrypt the form input before attempting to authenticate the user. It is important to note the the private_decrypt method wants binary data, so we need decode the Base64 text passed in the request (using Base64 seemed more appropriate than binary data here). After the authenticate method is called, things proceed as usual.

So far, this is working fairly well. There are a few options for improvement – perhaps a before_filter to preprocess any encrypted data. I’d be interested in hearing other ideas on this topic as well.

References


6 Comments on “Encrypting Login Password without SSL in Ruby on Rails”

  1. Micah says:

    How long did you spend working on this solution? Wouldn’t it have been “cheaper” just to get a certificate from Godaddy? They’re like $20/yr, I think. At $80/hr (a decent rate for programming) you could get a certificate for 4 years for the cost of a single hour of programming. Plus, you get a warm fuzzy feeling knowing that top security experts designed the entire system.

    I’m all for learning about encryption, but I don’t think rolling your own version of SSL is the way to go. Good security is tough stuff, and we should leave it to the pros when possible.

  2. josh says:

    A standard GoDaddy cert is $30/year and (1) I wanted to figure it out and (2) I’m cheap. #2 is really the more important factor here and it was just for a small personal site with little usage, so this solution works well enough.

  3. Atom says:

    Nice job. Yeah sometimes it’s all about figuring things out sure there’s the easier way but figuring things out helps come up with new things. For a client I did similar he wanted to see the exception messages of a legacy application but for security reasons we didn’t want to show them. So the exception handler encrypts the error messag with a password that lives on the server and then in a hidden div serves it. By clickin on the “PI” logo that’s conveniently a small image in the lower right corner it opens a modal box that when entered the proper password decodes the content of the div and displays the exception message. Problem solved. Client happy and we’re still all secure.

  4. Mike says:

    Any chance you will be adapting this for Flex?

  5. Tim Morgan says:

    I’m having trouble getting this to work with Internet Exploder. Keep getting “padding check failed” from OpenSSL. It works fine in Firefox.

  6. Tim Morgan says:

    Fixed: http://github.com/seven1m/onebody/commit/4be9c5e479dfd92103ad07a7ca9b3eb5e5faf61c

    It seems Internet Exploer + rsa.js + opensearch xml link tag do not play well together.